// 15 面向对象程序设计
/**
 * 面向对象程序设计基于三个基本概念：数据抽象、继承和动态绑定。第7章已经介绍了数据抽象的知识，本章将介绍继承和动态绑定。
 * 继承和动态绑定对程序的编写有两方面的影响：一是我们可以更容易地定义与其他类相似但不完全相同的新类；二是在使用这些彼此相似的类编写程序时，我们可以在一定程度上忽略掉它们的区别。
 * 在很多程序中都存在着一些相互关联但是有细微差别的概念。
 * 例如，书店中不同书籍的定价策略可能不同：有的书籍按原价销售，有的则打折销售。有时，我们给那些购买书籍超过一定数量的顾客打折；另一些时候，则只对前多少本销售的书籍打折，之后就调回原价，等等。
 * 面向对象的程序设计（OOP）适用于这类应用。
 */

#include <iterator>
#include <vector>
#include <list>
#include <deque>
#include <forward_list>
#include <string>
#include <array>
#include <stack>
#include <queue>
#include <algorithm>
#include <numeric>
using std::swap;
using std::vector, std::list, std::deque, std::forward_list, std::string, std::array, std::stack, std::queue;
#include "../Chapter07/Sales_data.h"
#include <iostream>
using std::begin, std::cbegin, std::end, std::cend, std::find, std::accumulate, std::equal, std::fill, std::fill_n, std::back_inserter;
using std::cin, std::cout, std::endl;
using std::copy, std::replace, std::replace_copy;
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <memory>
#include <new>
using namespace std;
#include "../Chapter13/13.5.cc" // 不能编译，因为重复定义的main函数
#include "../Chapter12/12.1.6.cc" // 不能编译，因为重复定义的main函数
#include <functional>

// 为了对之前提到的不同定价策略建模，我们首先定义一个名为Quote的类，并将它作为层次关系中的基类。
// Quote的对象表示按原价销售的书籍。Quote派生出另一个名为Bulk_quote的类，它表示可以打折销售的书籍。
class Quote
{
public:
    Quote() = default; // 关于=default参见7.1.4节（第237页）合成的默认构造函数
    Quote(const string &book, double sales_price) : bookNo(book), price(sales_price) {}
    // isbn（ ），返回书籍的ISBN编号。该操作不涉及派生类的特殊性，因此只定义在Quote类中。
    std::string isbn() const { return bookNo; }; // 常量成员函数承诺不修改其所属的对象的任何非静态数据成员。主要用途是允许对常量对象进行操作，同时保证不会改变这些对象的状态。
    // net_price（size_t），返回书籍的实际销售价格，前提是用户购买该书的数量达到一定标准。这个操作显然是类型相关的，Quote和Bulk_quote都应该包含该函数。
    virtual double net_price(std::size_t n) const { return n*price; };
    // 在C++语言中，基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数，基类希望它的派生类各自定义适合自身的版本，此时基类就将这些函数声明成虚函数（virtual function）。
    virtual ~Quote() = default; // 对析构函数进行动态绑定。根节点的类通常都会定义一个虚析构函数。
private:
    std::string bookNo; // 书籍的ISBN编号
protected:
    double price = 0.0; // 代表普通状态下不打折的价格
};
// 派生类必须通过使用类派生列表（class derivation list）明确指出它是从哪个（哪些）基类继承而来的。
// 类派生列表的形式是：首先是一个冒号，后面紧跟以逗号分隔的基类列表，其中每个基类前面可以有访问说明符：
class Bulk_quote : public Quote // Bulk_quote继承了Quote
{
public:
    Bulk_quote() = default;
    // 首先初始化基类的部分，然后按照声明的顺序依次初始化派生类的成员。
    // 当（空的）Quote构造函数体结束后，我们构建的对象的基类部分也就完成初始化了。接下来初始化由派生类直接定义的min_qty成员和discount成员。最后运行Bulk_quote构造函数的（空的）函数体。
    Bulk_quote(const string &book, double p, size_t qty, double disc) : Quote(book,p), min_qty(qty), discount(disc) {}; // 使用其基类构造函数初始化基类成员
    // 覆盖基类的函数版本以实现基于大量购买的折扣策略
    double net_price(std::size_t) const override; // 形参列表后增加override关键字
private:
    std::size_t min_qty = 0; // 使用折扣策略的最低购买量
    double discount = 0.0;   // 以小数表示的折扣额
};
// 计算并打印销售给定数量的某种书籍所得的费用
double print_total(ostream &os, const Quote &item, size_t n)
{
    // 根据传入item形参的对象类型调用Quote::net_price或者Bulk_quote::net_price
    double ret = item.net_price(n);
    os << "ISBN: " << item.isbn() // 调用Quote::isbin
       << " # sold: " << n << " total due: " << ret << endl;
    return ret;
}
// 如果达到了购买书籍的某个最低限量值，就可以享受折扣价格了
double Bulk_quote::net_price(std::size_t cnt) const
{
    if(cnt > min_qty)
        return cnt * (1-discount) * price;
    else
        return cnt * price;
}

int main()
{
    // 15.2 定义基类和派生类

    // 15.2.1 定义基类
    // 我们将在15.7.1节（第552页）详细介绍虚析构函数的知识，现在只需记住作为继承关系中根节点的类通常都会定义一个虚析构函数。
    // 基类通常都应该定义一个虚析构函数，即使该函数不执行任何实际操作也是如此。

    // 成员函数与继承
    // 当我们使用指针或引用调用虚函数时，该调用将被动态绑定。根据引用或指针所绑定的对象类型不同，该调用可能执行基类的版本，也可能执行某个派生类的版本。
    // 基类通过在其成员函数的声明语句之前加上关键字virtual使得该函数执行动态绑定。任何构造函数之外的非静态函数（参见7.6节，第268页）都可以是虚函数。
    // 如果基类把一个函数声明成虚函数，则该函数在派生类中隐式地也是虚函数。
    // virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。我们将在15.3节（第536页）介绍更多关于虚函数的知识。
    // 成员函数如果没被声明为虚函数，则其解析过程发生在编译时而非运行时。

    // 访问控制与继承
    // 和其他使用基类的代码一样，派生类能访问公有成员，而不能访问私有成员。
    // 不过在某些时候基类中还有这样一种成员，基类希望它的派生类有权访问该成员，同时禁止其他用户访问。我们用受保护的（protected）访问运算符说明这样的成员。
    // 我们将在15.5节（第542页）介绍更多关于受保护成员的知识。

    // 15.2.2 定义派生类
    // 类派生列表的形式是：首先是一个冒号，后面紧跟以逗号分隔的基类列表，其中每个基类前面可以有以下三种访问说明符中的一个：public、protected或者private。
    // 派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明，因此，我们的Bulk_quote类必须包含一个net_price成员：
    // 我们将在15.5节（第543页）详细介绍派生列表中用到的访问说明符。现在，我们只需知道访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类的用户可见。
    // 如果一个派生是公有的，则基类的公有成员也是派生类接口的组成部分。此外，我们能将公有派生类型的对象绑定到基类的引用或指针上。
    // 因为我们在派生列表中使用了public，所以Bulk_quote的接口隐式地包含isbn函数，同时在任何需要Quote的引用或指针的地方我们都能使用Bulk_quote的对象。
    // 关于派生列表中含有多于一个基类的情况将在18.3节（第710页）中介绍。

    // 派生类中的虚函数
    // 派生类经常（但不总是）覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数，则该虚函数的行为类似于其他的普通成员，派生类会直接继承其在基类中的版本。
    // 派生类可以在它覆盖的函数前使用virtual关键字，但不是非得这么做。我们将在15.3节（第538页）介绍其原因.

    // 派生类对象及派生类向基类的类型转换
    // 一个派生类对象包含多个组成部分：一个含有派生类自己定义的（非静态）成员的子对象，以及一个与该派生类继承的基类对应的子对象，如果有多个基类，那么这样的子对象也有多个。
    // 我们也能将基类的指针或引用绑定到派生类对象中的基类部分上。
    Quote item;       // 基类对象
    Bulk_quote bulk;  // 派生类对象
    Quote *p = &item; // p指向Quote对象
    p = &bulk;        // p指向bulk的Quote部分
    Quote &r = bulk;  // r绑定到bulk的Quote部分
    // 派生类到基类的（derived-to-base）类型转换。和其他类型转换一样，编译器会隐式地执行派生类到基类的转换（参见4.11节，第141页）。
    // 这种隐式特性意味着我们可以把派生类对象或者派生类对象的引用用在需要基类引用的地方；同样的，我们也可以把派生类对象的指针用在需要基类指针的地方。

    // 派生类构造函数
    // 尽管在派生类对象中含有从基类继承而来的成员，但是派生类并不能直接初始化这些成员。和其他创建了基类对象的代码一样，派生类也必须使用基类的构造函数来初始化它的基类部分。
    // 派生类对象的基类部分与派生类对象自己的数据成员都是在构造函数的初始化阶段（参见7.5.1节，第258页）执行初始化操作的。
    // 除非我们特别指出，否则派生类对象的基类部分会像数据成员一样执行默认初始化。如果想使用其他的基类构造函数，我们需要以类名加圆括号内的实参列表的形式为构造函数提供初始值。
    // 首先初始化基类的部分，然后按照声明的顺序依次初始化派生类的成员。

    // 派生类使用基类的成员
    // 派生类可以访问基类的公有成员和受保护成员：

    // 关键概念：遵循基类的接口
    // 因此，派生类对象不能直接初始化基类的成员。尽管从语法上来说我们可以在派生类构造函数体内给它的公有或受保护的基类成员赋值，但是最好不要这么做。
    // 通过调用基类的构造函数来初始化那些从基类中继承而来的成员。

    // 继承与静态成员
    // 如果基类定义了一个静态成员（参见7.6节，第268页），则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类，对于每个静态成员来说都只存在唯一的实例。
    // 静态成员遵循通用的访问控制规则，如果基类中的成员是private的，则派生类无权访问它。假设某静态成员是可访问的，则我们既能通过基类使用它也能通过派生类使用它：

    // 派生类的声明
    // 派生类的声明与其他类差别不大（参见7.3.3节，第250页），声明中包含类名但是不包含它的派生列表：
    //class Bulk_quote; // 正确，声明派生类的正确方式
    //class Bulk_quote : public Quote; // 错误，派生类列表不能出现在这里
    // 一条声明语句的目的是令程序知晓某个名字的存在以及该名字表示一个什么样的实体，如一个类、一个函数或一个变量等。

    // 被用作基类的类
    // 如果我们想将某个类用作基类，则该类必须已经定义而非仅仅声明：
    //class Quote1; // 声明但未定义
    //class Bulk_quote1 : public Quote1 {}; // 错误，Quote1必须被定义
    // 这一规定的原因显而易见：派生类中包含并且可以使用它从基类继承而来的成员，为了使用这些成员，派生类当然要知道它们是什么。因此该规定还有一层隐含的意思，即一个类不能派生它本身。
    // 一个类是基类，同时它也可以是一个派生类：
    // 每个类都会继承直接基类的所有成员。对于一个最终的派生类来说，它会继承其直接基类的成员；该直接基类的成员又含有其基类的成员；依此类推直至继承链的顶端。
    // 因此，最终的派生类将包含它的直接基类的子对象以及每个间接基类的子对象。

    // 防止继承的发生
    // C++11新标准提供了一种防止继承发生的方法，即在类名后跟一个关键字final：
    class NoDerived final{}; // NoDerived不能作为基类

    // 15.2.3 类型转换与继承
    // 我们可以将基类的指针或引用绑定到派生类对象上。例如，我们可以用Quote&指向一个Bulk_quote对象，也可以把一个Bulk_quote对象的地址赋给一个Quote＊。
    // 当使用基类的引用（或指针）时，实际上我们并不清楚该引用（或指针）所绑定对象的真实类型。该对象可能是基类的对象，也可能是派生类的对象。
    // 我们可以将一个派生类对象的指针存储在一个基类的智能指针内。

    // 静态类型与动态类型
    // 当我们使用存在继承关系的类型时，必须将一个变量或其他表达式的静态类型（static type）与该表达式表示对象的动态类型（dynamic type）区分开来。
    // 表达式的静态类型在编译时总是已知的，它是变量声明时的类型或表达式生成的类型；动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。
    // 如果表达式既不是引用也不是指针，则它的动态类型永远与静态类型一致。例如，Quote类型的变量永远是一个Quote对象，我们无论如何都不能改变该变量对应的对象的类型。
    // 基类的指针或引用的静态类型可能与其动态类型不一致，读者一定要理解其中的原因。

    // 不存在基类向派生类的隐式类型转换
    // 之所以存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分，而基类的引用或指针可以绑定到该基类部分上。
    // 一个基类的对象既可以以独立的形式存在，也可以作为派生类对象的一部分存在。如果基类对象不是派生类对象的一部分，则它只含有基类定义的成员，而不含有派生类定义的成员。
    // 因为一个基类的对象可能是派生类对象的一部分，也可能不是，所以不存在从基类向派生类的自动类型转换：
    Quote base;
    //Bulk_quote *bulkP = &base; // 错误，不能将基类转换成派生类
    //Bulk_quote &bulkRef = base; // 错误，不能将基类转换成派生类
    // 如果上述赋值是合法的，则我们有可能会使用bulkP或bulkRef访问base中本不存在的成员。
    // 除此之外还有一种情况显得有点特别，即使一个基类指针或引用绑定在一个派生类对象上，我们也不能执行从基类向派生类的转换：
    Bulk_quote bulk1;
    Quote *itemP = &bulk1; // 正确，动态类型是Bulk_quote
    //Bulk_quote *bulkP1 = itemP; // 错误，不能将基类转换成派生类
    // 如果我们已知某个基类向派生类的转换是安全的，则我们可以使用static_cast（参见4.11.3节，第144页）来强制覆盖掉编译器的检查工作。

    // ……在对象之间不存在类型转换
    // 派生类向基类的自动类型转换只对指针或引用类型有效，在派生类类型和基类类型之间不存在这样的转换。
    // 请注意，当我们初始化或赋值一个类类型的对象时，实际上是在调用某个函数。
    // 当执行初始化时，我们调用构造函数（参见13.1.1节，第440页和13.6.2节，第473页）；而当执行赋值操作时，我们调用赋值运算符（参见13.1.2节，第443页和13.6.2节，第474页）。
    // 当我们用一个派生类对象为一个基类对象初始化或赋值时，只有该派生类对象中的基类部分会被拷贝、移动或赋值，它的派生类部分将被忽略掉。

    // 关键概念：存在继承关系的类型之间的转换规则要想理解在具有继承关系的类之间发生的类型转换，有三点非常重要：
    // · 从派生类向基类的类型转换只对指针或引用类型有效。
    // · 基类向派生类不存在隐式类型转换。
    // · 和任何其他成员一样，派生类向基类的类型转换也可能会由于访问受限而变得不可行。我们将在15.5节（第544页）详细介绍可访问性的问题。
    // 尽管自动类型转换只对指针或引用类型有效，但是继承体系中的大多数类仍然（显式或隐式地）定义了拷贝控制成员（参见第13章）。
    // 因此，我们通常能够将一个派生类对象拷贝、移动或赋值给一个基类对象。不过需要注意的是，这种操作只处理派生类对象的基类部分。




    return 0;
}